import HeatmapValue from "./HeatmapValue"; /** * 将颜色名称或HEX转换为标准的6位HEX格式 * @param color 颜色名称或HEX值 * @returns 6位HEX颜色代码 */ function colorToHex(color: string): string { // 如果是HEX格式直接返回 if (/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color)) { if (color.length === 4) { // 缩写格式如 #fff return `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`; } return color; } // 创建临时div元素来获取颜色值 (仅在浏览器环境中有效) if (typeof document === 'undefined') { // 在非浏览器环境 (如 Node.js) 可能需要其他方式或预定义的颜色映射 // 这里简化处理,如果不在浏览器环境且不是HEX,则抛错 throw new Error(`无法在当前环境解析颜色名称: ${color}`); } const tempDiv = document.createElement('div'); tempDiv.style.color = color; document.body.appendChild(tempDiv); // 获取计算后的颜色值 const computedColor = window.getComputedStyle(tempDiv).color; document.body.removeChild(tempDiv); // 解析rgb/rgba值 const rgbMatch = computedColor.match(/^rgb(a?)\((\d+),\s*(\d+),\s*(\d+)(?:,\s*\d+\.?\d*)?\)$/i); if (rgbMatch) { const r = parseInt(rgbMatch[2]).toString(16).padStart(2, '0'); const g = parseInt(rgbMatch[3]).toString(16).padStart(2, '0'); const b = parseInt(rgbMatch[4]).toString(16).padStart(2, '0'); return `#${r}${g}${b}`.toLowerCase(); } throw new Error(`无法解析颜色: ${color}`); } /** * 在两个HEX颜色之间插值 * @param startHex 起始颜色 * @param endHex 结束颜色 * @param factor 插值因子 (0-1) * @returns 插值后的HEX颜色 */ function interpolateColor(startHex: string, endHex: string, factor: number): string { // 确保 factor 在 0 到 1 之间 factor = Math.max(0, Math.min(1, factor)); // 去除#号并解析RGB分量 const start = startHex.substring(1).match(/.{2}/g)!.map(c => parseInt(c, 16)); const end = endHex.substring(1).match(/.{2}/g)!.map(c => parseInt(c, 16)); // 计算插值 const r = Math.round(start[0] + factor * (end[0] - start[0])); const g = Math.round(start[1] + factor * (end[1] - start[1])); const b = Math.round(start[2] + factor * (end[2] - start[2])); // 返回HEX格式 return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; } /** * 创建一个将数值映射到离散颜色梯度的函数 (类似 d3.scaleQuantize) * @param minValue 数值范围最小值 * @param maxValue 数值范围最大值 * @param colors 颜色数组,表示渐变的颜色停靠点 (HEX 或颜色名称) * @param numColors 期望的离散颜色数量 ( quantize 标度的值域大小) * @returns 一个函数,接收一个数值并返回对应的颜色字符串 */ function createQuantizeColorScale( minValue: number, maxValue: number, colors: string[], // 颜色数组 numColors: number // 期望的离散颜色数量 ): (value: number) => string { // 确保颜色数组至少有两个颜色才能形成渐变,否则使用第一个颜色 if (colors.length < 2) { const singleColorHex = colors.length > 0 ? colorToHex(colors[0]) : '#000000'; // 默认黑色 // 如果颜色少于2个,或者 numColors 为 1,都只返回一个颜色 return (value: number): string => singleColorHex; } // 确保 numColors 是正数 if (numColors <= 0) { numColors = 1; } // 将所有颜色转换为HEX格式 const hexColors = colors.map(colorToHex); // 生成 numColors 个离散颜色作为 quantize 标度的值域 const colorRange: string[] = []; if (numColors === 1) { // 如果只要求一种颜色,使用颜色数组的中间色 const middleColorIndex = Math.floor((hexColors.length - 1) / 2); colorRange.push(hexColors[middleColorIndex]); } else { // 在提供的颜色之间进行插值,生成 numColors 个颜色 for (let i = 0; i < numColors; i++) { // 计算在整个颜色范围内的比例 (0 到 1) const globalFactor = i / (numColors - 1); // 计算对应的颜色段 const colorSegment = globalFactor * (hexColors.length - 1); const colorIndex = Math.floor(colorSegment); const segmentFactor = colorSegment - colorIndex; // 在当前颜色段内的比例 // 确保索引不会超出范围 const startColorIndex = Math.max(0, Math.min(hexColors.length - 2, colorIndex)); const endColorIndex = startColorIndex + 1; const startColor = hexColors[startColorIndex]; const endColor = hexColors[endColorIndex]; colorRange.push(interpolateColor(startColor, endColor, segmentFactor)); } } // 返回一个颜色比例尺函数 return (value: number): string => { // 处理边界情况 if (value <= minValue) return colorRange[0]; if (value >= maxValue) return colorRange[colorRange.length - 1]; // 如果 min 和 max 相同,所有值都映射到第一个颜色 (或中间色如果 numColors === 1) if (minValue === maxValue) { return colorRange[0]; } // 计算值在范围内的比例 (0 到 1) const proportion = (value - minValue) / (maxValue - minValue); // 计算对应的颜色段索引 const colorIndex = Math.floor(proportion * (colorRange.length - 1)); // 返回对应的颜色 // 确保索引不会超出范围 return colorRange[Math.max(0, Math.min(colorRange.length - 1, colorIndex))]; }; } /** * Preprocesses input data to extract numeric values, determine color scale, * and map each numeric value to a color based on the scale. * @param data Input data array (can contain numbers or HeatmapValue objects). * @param colors An array of colors defining the gradient stops. At least two colors are recommended. * @param colorValues The number of discrete colors in the gradient. * @param valueRange Optional: A tuple [minValue, maxValue] to manually specify the value range for color mapping. If not provided, the range is calculated from the data. * @returns A 2D array of HeatmapValue objects with colors assigned. */ export function preprocessValues( data: (number | HeatmapValue)[][], colors: string[], // 颜色数组 colorValues: number, // 期望的离散颜色数量 valueRange: number[] = [] // 手动指定数值范围,默认为空数组 ): HeatmapValue[][] { // 确保 colorValues 是正数 if (colorValues <= 0) { colorValues = 1; // 至少为1 } const allNumericValues: number[] = []; data.forEach(row => { row.forEach(cell => { if (typeof cell === "number") { allNumericValues.push(cell); } else if (cell && typeof cell.value === 'number') { // cell 是一个包含 value 属性的 HeatmapValue 对象 allNumericValues.push(cell.value); } }); }); let minValue: number; let maxValue: number; // 根据 valueRange 参数确定数值范围 if (valueRange && valueRange.length === 2 && typeof valueRange[0] === 'number' && typeof valueRange[1] === 'number') { minValue = valueRange[0]; maxValue = valueRange[1]; } else { minValue = Math.min(...allNumericValues); maxValue = Math.max(...allNumericValues); // // 如果没有手动指定范围,则从数据中计算 // if (allNumericValues.length === 0) { // // 如果没有数值数据且没有手动指定范围,则默认范围为 [0, 0] // minValue = 0; // maxValue = 0; // } else { // } } console.log('min:',minValue,'max',maxValue); // 如果没有数值数据,或者手动指定范围后数据都在范围外,则对任何数字应用默认颜色 // 默认颜色使用颜色数组的第一个颜色 (如果颜色数组不为空) if (allNumericValues.length === 0 && !(valueRange && valueRange.length === 2)) { const defaultColor = colors.length > 0 ? colorToHex(colors[0]) : '#000000'; // 默认黑色 return data.map((row, rowIdx) => row.map((cell, colIdx) => { if (typeof cell === "number") { return { value: cell, rowId: rowIdx, columnId: colIdx, color: defaultColor }; } // 如果 cell 不是数字,保持原样 (假设已经是 HeatmapValue 或其他类型) return cell as HeatmapValue; })); } // 创建颜色比例尺函数 const colorScale = createQuantizeColorScale( minValue, maxValue, colors, // 使用颜色数组 colorValues ); // 映射数据到 HeatmapValue 对象 const datas = data.map((row, rowIdx) => { return row.map((cell, colIdx) => { if (typeof cell === "number") { return { value: cell, rowId: rowIdx, columnId: colIdx, color: colorScale(cell) // 使用新的颜色比例尺 }; } else if (cell && typeof cell.value === 'number') { // 如果已经是 HeatmapValue 对象并且有数值 value,则重新计算其颜色 // 同时保留可能已存在的 rowId 和 columnId return { ...cell, rowId: cell.rowId !== undefined ? cell.rowId : rowIdx, columnId: cell.columnId !== undefined ? cell.columnId : colIdx, color: colorScale(cell.value) // 使用新的颜色比例尺 }; } // 如果 cell 不是数字,也不是包含数字 value 的 HeatmapValue 对象,则原样返回 return cell as HeatmapValue; }); }); console.log(`datas ===========>`,datas); return datas }